import {
    html,
    SpectrumElement,
} from "../tools/base";
import {ifDefined} from "../tools/base/directives";
import {reparentChildren} from "../tools/base/reparent-children.js";
import {firstFocusableIn} from "../tools/base/first-focusable-in.js";
import styles from './active-overlay.css.js';
import {parentOverlayOf} from './overlay-utils.js';
import {
    arrow,
    autoUpdate,
    computePosition,
    flip,
    offset,
    shift,
    size,
} from '@floating-ui/dom';

/**
 * @typedef {'idle' | 'active' | 'hiding' | 'dispose' | 'disposed'} OverlayStateType
 */

/**
 * @typedef {{[stateName: string]: {
 *             on: {
 *                 [transitionName: string]: OverlayStateType;
 *             };
 *         }}} StateMachineStates
 */

/**
 * 状态机
 * @type {object}
 * @property {OverlayStateType} initial
 * @property {StateMachineStates} states
 */
const stateMachine = {
    initial: 'idle',
    states: {
        idle: {
            on: {
                active: 'active',
            }
        },
        active: {
            on: {
                hiding: 'hiding',
                idle: 'idle',
            }
        },
        hiding: {
            on: {
                dispose: 'dispose',
            }
        },
        dispose: {
            on: {
                disposed: 'disposed',
            }
        },
        disposed: {
            on: {}
        }
    }
}

/**
 * 状态转换
 * @param state {string: 'idle' | 'active' | 'hiding' | 'dispose' | 'disposed' | undefined}
 * @param event {string | undefined}
 */
const stateTransition = (state = undefined,
                         event = undefined) => {
    if (!state) return stateMachine.initial;
    if (!event) return state;
    return stateMachine.states[state].on[event] || state;
}

/**
 *
 * @typedef {string: 'top' | 'right' | 'bottom' | 'left' |
 * 'top-start' | 'right-start' | 'bottom-start' | 'left-start' |
 * 'top-end' | 'right-end' | 'bottom-end' | 'left-end'} FloatingUIPlacement
 * @param placement {FloatingUIPlacement}
 * @return {FloatingUIPlacement[]}
 */
const getFallbackPlacements = (placement) => {
    const fallbacks = {
        left: ['right', 'bottom', 'top'],
        'left-start': ['right-start', 'bottom', 'top'],
        'left-end': ['right-end', 'bottom', 'top'],
        right: ['left', 'bottom', 'top'],
        'right-start': ['left-start', 'bottom', 'top'],
        'right-end': ['left-end', 'bottom', 'top'],
        top: ['bottom', 'left', 'right'],
        'top-start': ['bottom-start', 'left', 'right'],
        'top-end': ['bottom-end', 'left', 'right'],
        bottom: ['top', 'left', 'right'],
        'bottom-start': ['top-start', 'left', 'right'],
        'bottom-end': ['top-end', 'left', 'right'],
    };
    return fallbacks[placement] ?? [placement];
}

/**
 * @element active-overlay
 * @slot - content to display in the overlay
 */
export class ActiveOverlay extends SpectrumElement {
    /**
     *
     * @type {HTMLElement}
     */
    overlayContent = null;
    /**
     *
     * @type {HTMLElement | undefined}
     */
    overlayContentTip;
    /**
     *
     * @type {HTMLElement}
     */
    trigger = null;

    /**
     *
     * @type {ActiveOverlay | undefined}
     */
    root;

    /**
     *
     * @type {HTMLElement | undefined}
     */
    virtualTrigger;

    /**
     * @private
     * @returns {function():void | undefined}
     */
    cleanup;

    /**
     * @protected
     * @type {Promise<boolean>}
     */
    contentAnimationPromise = Promise.resolve(true);

    /**
     * @protected
     * @type {function:void}
     */
    resolveContentAnimationPromise = () => {
    };
    /**
     * @public
     * @type {boolean}
     */
    tabbingAway = false;

    /**
     * @private
     * @type {FloatingUIPlacement | undefined}
     */
    originalPlacement;
    /**
     * @private
     * @type {function():Element[] | undefined}
     */
    restoreContent;

    static properties = {
        state: {
            type: String,
        },
        animating: {
            type: Boolean,
            reflect: true,
        },
        placement: {
            type: String,
            reflect: true,
        },
        /**
         * @typedef {{color: 'light'
         *     | 'lightest'
         *     | 'dark'
         *     | 'darkest'
         *     | 'light-express'
         *     | 'lightest-express'
         *     | 'dark-express'
         *     | 'darkest-express' | undefined,
         *     scale: 'medium' | 'large'
         *     | 'medium-express' | 'large-express' | undefined,
         *     lang: string | undefined,
         *     theme: 'spectrum' | 'express' | undefined
         *     }} theme
         */
        theme: {
            type: Object,
            attribute: false,
        },
        receivesFocus: {
            type: String,
            attribute: false,
        },
    };

    /**
     * @private
     * @type {Promise<void>}
     */
    stealOverlayContentPromise = Promise.resolve();

    /**
     * @private
     * @type {()=>void}
     */
    stealOverlayContentResolver = () => {
    };

    /**
     *
     * @private
     * @type {ActiveOverlay | undefined}
     */
    _modalRoot;

    constructor() {
        super();
        this._state = stateTransition();
        this.stealOverlayContentPromise = new Promise((res) => {
            this.stealOverlayContentResolver = res;
        })
    }

    /**
     * @return {"idle"|"active"|"hiding"|"dispose"|"disposed"}
     */
    get state() {
        return this._state;
    }

    set state(state) {
        const nextState = stateTransition(this.state, state);
        if (nextState === this.state) {
            return;
        }
        this._state = nextState;
        if (this.state === 'active' || this.state === 'hiding') {
            this.setAttribute('state', this.state);
        } else {
            this.removeAttribute('state');
        }
    }

    /**
     * @public
     * @returns {Promise<void>}
     */
    async focus() {
        const firstFocusable = firstFocusableIn(this);
        if (firstFocusable) {
            if (firstFocusable.updateComplete) {
                await firstFocusable.updateComplete;
            }
            const activeElement = this.getRootNode().activeElement;
            if (activeElement === this || !this.contains(activeElement)) {
                firstFocusable.focus();
            }
        } else {
            super.focus();
        }
        this.removeAttribute('tabindex');
    }

    /**
     * @private
     * @returns {boolean}
     */
    get hasTheme() {
        return !!this.theme.color ||
            !!this.theme.scale ||
            !!this.theme.lang;
    }

    /**
     * @public
     * @type {number}
     */
    offset = 6;

    /**
     * @public
     * @type {number}
     */
    skidding = 0;

    /**
     * @public
     * @type {string}
     */
    interaction = 'hover';

    /**
     * @private
     * @type {number}
     */
    positionAnimationFrame = 0;

    /**
     * @private
     * @type {number | undefined}
     */
    timeout;

    /**
     * @typedef {CSSStyleSheet} CSSResult
     */

    /**
     * @typedef {CSSResult[]} CSSResultArray
     */

    /**
     * @public
     * @override
     * @return {CSSResultArray}
     */
    static get styles() {
        return [styles];
    }

    feature() {
        if (!this.contains(document.activeElement)) {
            this.tabIndex = -1;
        }
        const parentOverlay = parentOverlayOf(this.trigger);
        const parentIsModal = parentOverlay && parentOverlay.slot === 'open';
        if (parentIsModal) {
            this._modalRoot = parentOverlay._modalRoot || parentOverlay;
        }
        //如果一个叠加是从一个“模态”叠加中触发的，它需要继续
        //在TAB捕获方面得到正确的处理。
        if (this.interaction === 'modal' || this._modalRoot) {
            this.slot = 'open';
            if (this.interaction === 'modal') {
                this.setAttribute('aria-modal', 'true');
            }
            //如果这不是一个模态根，向上走到下一个模态根
            //和“feature”每个中间的叠加。
            if (this._modalRoot) {
                parentOverlay?.feature();
            }
        }
    }

    /**
     * @function obscure
     * @public
     * @typedef TriggerInteractions {string: 'click' |
     * 'longpress' | 'hover' | 'custom' | 'replace' | 'inline' | 'modal'}
     * @param nextOverlayInteraction {TriggerInteractions}
     * @return {ActiveOverlay | undefined}
     */
    obscure(nextOverlayInteraction) {
        if (this.slot && nextOverlayInteraction === 'modal') {
            this.removeAttribute('slot');
            this.removeAttribute("aria-modal");
            if (this.interaction !== 'modal') {
                const parentOverlay = parentOverlayOf(this.trigger);
                this._modalRoot = parentOverlay && parentOverlay.obscure(
                    nextOverlayInteraction
                );
                return this._modalRoot;
            }
            return this;
        }
        return undefined;
    }

    /**
     * @public
     * @override
     * @function willUpdate
     */
    async willUpdate() {
        if (this.hasUpdated) return;
        if (!this.overlayContent || !this.trigger) return;
        this.stealOverlayContent(
            this.overlayContent
        )
        this.state = 'active';
        this.feature();
        if (this.placement && this.placement !== 'none') {
            await this.updateOverlayPosition();
            document.addEventListener(
                'sp-update-overlays',
                this.resetOverlayPosition
            );
        }
        if (this.placement && this.placement !== 'none') {
            this.contentAnimationPromise =
                this.applyContentAnimation('sp-overlay-fade-in');
        }
    }

    /**
     * @typedef {FloatingUIPlacement | 'none'} Placement
     */

    /**
     * @private
     * @function stealOverlayContent
     * @typedef {FloatingUIPlacement | 'none'} Placement
     * @param {HTMLElement & { placement: Placement }} element
     */
    stealOverlayContent(element) {
        this.originalPlacement = element.getAttribute('placement');
        this.restoreContent = reparentChildren([element], this, {
            position: 'beforeend',
            prepareCallback: (el) => {
                const slotName = el.slot;
                const placement = el.placement;
                el.removeAttribute('slot');
                return (el) => {
                    el.slot = slotName;
                    el.placement = placement;
                };
            },
        });
        this.stealOverlayContentResolver();
    }

    /** @private {boolean} willNotifyClosed */
    willNotifyClosed = false;

    /**
     * @private
     * @function returnOverlayContent
     */
    returnOverlayContent() {
        if (!this.restoreContent) return;
        const [element] = this.restoreContent();
        this.restoreContent = undefined;
        this.willNotifyClosed = true;
        if (this.originalPlacement) {
            element.setAttribute('placement', this.originalPlacement);
            delete this.originalPlacement;
        }
    }

    /** @private {number | undefined} */
    initialHeight;
    /** @private {boolean} */
    isConstrained = false;

    /**
     * 更新Overlay定位
     *
     * @public
     * @function updateOverlayPosition
     */
    updateOverlayPosition = () => {
        if (this.interaction !== 'modal' && this.cleanup) {
            this.dispatchEvent(new Event('close'));
            return;
        }
        this.setOverlayPosition();
    }

    /**
     * @public
     * @async
     * @function setOverlayPosition
     * @return {Promise<void>}
     */
    async setOverlayPosition() {
        if (!this.placement || this.placement === 'none') {
            return;
        }
        // 在所有字体加载完成后进行操作
        await (document.fonts ? document.fonts.ready : Promise.resolve());

        /**
         * @private
         * @function roundByDPR
         * @param num {number}
         * @return {number|number}
         */
        function roundByDPR(num) {
            const dpr = window.devicePixelRatio || 1;
            return Math.round(num * dpr) / dpr || -10000;
        }

        // See: https://spectrum.adobe.com/page/popover/#Container-padding
        const REQUIRED_DISTANCE_TO_EDGE = 8;
        // See: https://github.com/adobe/spectrum-web-components/issues/910
        const MIN_OVERLAY_HEIGHT = 100;
        // flip:通过翻转位置来优化浮动元素的可见性，
        // 以便在首选位置超出剪切边界时将其保留在视图中。替代自动放置。
        const flipMiddleware = this.virtualTrigger
            ? flip({
                padding: REQUIRED_DISTANCE_TO_EDGE,
                fallbackPlacements: getFallbackPlacements(this.placement),
            })
            : flip({
                padding: REQUIRED_DISTANCE_TO_EDGE,
            });
        const middleware = [
            offset({
                mainAxis: this.offset,
                crossAxis: this.skidding,
            }),
            shift({padding: REQUIRED_DISTANCE_TO_EDGE}),
            flipMiddleware,
            size({
                padding: REQUIRED_DISTANCE_TO_EDGE,
                apply: ({
                            availableWidth,
                            availableHeight,
                            rects: {floating},
                        }) => {
                    const maxHeight = Math.max(
                        MIN_OVERLAY_HEIGHT,
                        Math.floor(availableHeight)
                    );
                    const actualHeight = floating.height;
                    this.initialHeight =
                        !this.isConstrained && !this.virtualTrigger
                            ? actualHeight
                            : this.initialHeight || actualHeight;
                    this.isConstrained =
                        actualHeight < this.initialHeight ||
                        maxHeight <= actualHeight;
                    const appliedHeight = this.isConstrained
                        ? `${maxHeight}px`
                        : '';
                    Object.assign(this.style, {
                        maxWidth: `${Math.floor(availableWidth)}px`,
                        maxHeight: appliedHeight,
                        height: appliedHeight,
                    });
                },
            }),
        ];
        if (this.overlayContentTip) {
            middleware.push(arrow({element: this.overlayContentTip}));
        }
        const {x, y, placement, middlewareData} = await computePosition(
            this.virtualTrigger || this.trigger,
            this,
            {
                placement: this.placement,
                middleware,
                strategy: 'fixed',
            }
        );

        Object.assign(this.style, {
            top: '0px',
            left: '0px',
            transform: `translate(${roundByDPR(x)}px, ${roundByDPR(y)}px)`,
        });

        if (placement !== this.getAttribute('actual-placement')) {
            this.setAttribute('actual-placement', placement);
            this.overlayContent.setAttribute('placement', placement);
        }

        if (this.overlayContentTip && middlewareData.arrow) {
            const {x: arrowX, y: arrowY} = middlewareData.arrow;

            Object.assign(this.overlayContentTip.style, {
                left: arrowX != null ? `${roundByDPR(arrowX)}px` : '',
                top: arrowY != null ? `${roundByDPR(arrowY)}px` : '',
                right: '',
                bottom: '',
            });
        }
    }

    /**
     * @public
     * @function hide
     * @param animated {boolean}
     * @return {Promise<void>}
     */
    async hide(animated = true) {
        if (this.state !== 'active') return;
        this.state = 'hiding';
        if (animated) {
            await this.applyContentAnimation('sp-overlay-fade-out');
        }
        this.state = 'dispose';
    }

    /**
     * 调度定位更新
     *
     * @private
     * @function schedulePositionUpdate
     */
    schedulePositionUpdate() {
        // Edge needs a little time to update the DOM before computing the layout
        cancelAnimationFrame(this.positionAnimationFrame);
        this.positionAnimationFrame = requestAnimationFrame(() => {
            if (this.cleanup) {
                this.updateOverlayPosition();
            } else {
                this.placeOverlay();
            }
        });
    }

    /**
     * @private
     * @function onSlotChange
     */
    onSlotChange() {
        this.schedulePositionUpdate();
    }

    /**
     * @public
     * @function handleInlineTriggerKeydown
     * @param event {KeyboardEvent}
     */
    handleInlineTriggerKeydown(event) {
        const {code, shiftKey} = event;
        /* c8 ignore next */
        if (code !== 'Tab') return;
        if (shiftKey) {
            this.tabbingAway = true;
            this.dispatchEvent(new Event('close'));
            return;
        }

        event.stopPropagation();
        event.preventDefault();
        this.focus();
    }

    /**
     * 动画类型
     * @typedef {'sp-overlay-fade-in' | 'sp-overlay-fade-out'} ContentAnimation
     */

    /**
     * @public
     * @function applyContentAnimation
     * @param animation {ContentAnimation}
     * @return {Promise<boolean>|Promise<unknown>}
     */
    applyContentAnimation(animation) {
        if (this.placement === 'none') {
            return Promise.resolve(true);
        }
        this.resolveContentAnimationPromise();
        return new Promise((resolve) => {
            this.resolveContentAnimationPromise = () => {
                resolve(false);
            };
            /** @type {HTMLElement} */
            const contents = this.shadowRoot.querySelector(
                '#contents'
            );
            /**
             *
             * @param event {AnimationEvent}
             */
            const doneHandler = (event) => {
                if (animation !== event.animationName) return;
                contents.removeEventListener('animationend', doneHandler);
                contents.removeEventListener('animationcancel', doneHandler);
                this.animating = false;
                resolve(event.type === 'animationcancel');
            };
            contents.addEventListener('animationend', doneHandler);
            contents.addEventListener('animationcancel', doneHandler);

            contents.style.animationName = animation;
            this.animating = true;
        });
    }

    /**
     * @public
     * @function renderTheme
     * @param content {TemplateResult}
     * @return {TemplateResult<1>}
     */
    renderTheme(content) {
        const {color, scale, lang, theme} = this.theme;
        return html`
            <sp-theme
                    theme=${ifDefined(theme)}
                    color=${ifDefined(color)}
                    scale=${ifDefined(scale)}
                    lang=${ifDefined(lang)}
                    part="theme"
            >
                ${content}
            </sp-theme>
        `;
    }

    /**
     * @public
     * @function render
     * @override
     * @return {TemplateResult}
     */
    render() {
        const content = html`
            <div id="contents">
                <slot @slotchange=${this.onSlotChange}></slot>
            </div>
        `;
        return this.hasTheme ? this.renderTheme(content) : content;
    }

    /**
     * @static
     * @function create
     * @param details {OverlayOpenDetail}
     * @return {ActiveOverlay}
     */
    static create(details) {
        const overlay = new ActiveOverlay();

        if (details.content) {
            overlay.open(details);
        }

        return overlay;
    }

    /**
     * @private
     * @param {OverlayOpenDetail} openDetail
     */
    open(openDetail) {
        this.extractDetail(openDetail);
    }

    /**
     * @private
     * @param {OverlayOpenDetail} detail
     */
    extractDetail(detail) {
        this.overlayContent = detail.content;
        this.overlayContentTip = detail.contentTip;
        this.trigger = detail.trigger;
        this.virtualTrigger = detail.virtualTrigger;
        this.placement = detail.placement;
        this.offset = detail.offset;
        this.skidding = detail.skidding || 0;
        this.interaction = detail.interaction;
        this.theme = detail.theme;
        this.receivesFocus = detail.receivesFocus;
        this.root = detail.root;
    }

    /**
     * @async
     * @function getUpdateComplete
     * @override
     * @return {Promise<boolean>}
     */
    async getUpdateComplete() {
        /**
         *
         * @type {(Promise<unknown>)[]}
         */
        const actions = [
            super.getUpdateComplete(),
            this.stealOverlayContentPromise,
        ];
        actions.push(this.contentAnimationPromise);
        if (
            typeof this.overlayContent.updateComplete !==
            'undefined'
        ) {
            actions.push(
                this.overlayContent.updateComplete
            );
        }
        const [complete] = await Promise.all(actions);
        return complete;
    }

    /**
     * @public
     * @return {Promise<void>}
     */
    async placeOverlay() {
        if (!this.placement || this.placement === 'none') {
            return;
        }

        this.cleanup = autoUpdate(
            this.virtualTrigger || this.trigger,
            this,
            this.updateOverlayPosition,
            {
                elementResize: false,
            }
        );
    }

    /**
     * @public
     * @method openCallback
     * @param {() => Promise<void> | void} lifecycleCallback
     * @return {Promise<void>}
     */
    async openCallback(lifecycleCallback) {
        await this.updateComplete;
        if (this.receivesFocus) {
            await this.focus();
        }

        await lifecycleCallback();

        this.trigger.dispatchEvent(
            /** @type {CustomEvent<OverlayOpenCloseDetail>} */
            new CustomEvent('sp-opened', {
                bubbles: true,
                composed: true,
                cancelable: true,
                detail: {
                    interaction: this.interaction,
                },
            })
        );
    }

    /**
     * @public
     * @method dispose
     */
    dispose() {
        /* c8 ignore next */
        if (this.state !== 'dispose') return;

        /* c8 ignore next 4 */
        if (this.timeout) {
            clearTimeout(this.timeout);
            delete this.timeout;
        }

        this.trigger.removeEventListener(
            'keydown',
            this.handleInlineTriggerKeydown
        );

        this.returnOverlayContent();
        this.state = 'disposed';

        if (this.willNotifyClosed) {
            this.overlayContent.dispatchEvent(new Event('sp-overlay-closed'));
            this.willNotifyClosed = false;
        }

        if (this.cleanup) {
            this.cleanup();
        }
    }
}

customElements.define("active-overlay", ActiveOverlay);
